﻿using System;
using System.Collections.Generic;
using System.Linq;
using Properties;

namespace BorgExt
{
    public class TypeStore : RuntimeElement, ITypeStoreReader, ITypeStoreObserver
    {
        private readonly MetaElementCollection<Contract> _contracts;
        private readonly MetaElementCollection<Plugin> _plugins;

        protected readonly Dictionary<string, MetaElementCollection<Contract>> contractDictionary;
        protected readonly Dictionary<string, MetaElementCollection<SlotDefinition>> slotDefinitionDictionary;
        protected readonly Dictionary<string, MetaElementCollection<Plugin>> pluginDictionary;

        protected readonly Dictionary<string, MetaElementCollection<ExtensionType>> extensionTypeDictionary;
        protected readonly Dictionary<string, MetaElementCollection<PlugType>> plugTypeDictionary;
        protected readonly Dictionary<string, MetaElementCollection<SlotType>> slotTypeDictionary;

        internal ContractEventHandler[] ContractAddedEventHandlers { get; set; }
        internal PluginEventHandler[] PluginAddedEventHandlers { get; set; }

        internal ContractEventHandler[] ContractRemovedEventHandlers { get; set; }
        internal PluginEventHandler[] PluginRemovedEventHandlers { get; set; }

        protected internal TypeStoreModifier Modifier { get; protected set; }

        public IEnumerable<Contract> Contracts
        {
            get
            {
                lock (LockObject)
                    return _contracts;
            }
        }

        public IEnumerable<Plugin> Plugins
        {
            get
            {
                lock (LockObject)
                    return _plugins;
            }
        }

        public IEnumerable<SlotDefinition> SlotDefinitions
        {
            get
            {
                lock (LockObject)
                    return _contracts.SelectMany(contract =>
                        (IEnumerable<SlotDefinition>)contract.SlotDefinitions);
            }
        }

        public IEnumerable<ExtensionType> ExtensionTypes
        {
            get
            {
                lock (LockObject)
                    return _plugins.SelectMany(plugin =>
                        (IEnumerable<ExtensionType>)plugin.ExtensionTypes);
            }
        }

        public IEnumerable<PlugType> PlugTypes
        {
            get
            {
                lock (LockObject)
                    return _plugins.SelectMany(plugin =>
                        plugin.ExtensionTypes.SelectMany(extensionType =>
                            (IEnumerable<PlugType>)extensionType.PlugTypes));
            }
        }

        public IEnumerable<SlotType> SlotTypes
        {
            get
            {
                lock (LockObject)
                    return _plugins.SelectMany(plugin =>
                        (IEnumerable<ExtensionType>)plugin.ExtensionTypes).SelectMany(extensionType =>
                            (IEnumerable<SlotType>)extensionType.SlotTypes);
            }
        }

        public event ContractEventHandler ContractAdded
        {
            add
            {
                lock (LockObject)
                    ContractAddedEventHandlers = ArrayEx.Add(ContractAddedEventHandlers, value);
            }
            remove
            {
                lock (LockObject)
                    ContractAddedEventHandlers = ArrayEx.Remove(ContractAddedEventHandlers, value);
            }
        }

        public event PluginEventHandler PluginAdded
        {
            add
            {
                lock (LockObject)
                    PluginAddedEventHandlers = ArrayEx.Add(PluginAddedEventHandlers, value);
            }
            remove
            {
                lock (LockObject)
                    PluginAddedEventHandlers = ArrayEx.Remove(PluginAddedEventHandlers, value);
            }
        }

        public event ContractEventHandler ContractRemoved
        {
            add
            {
                lock (LockObject)
                    ContractRemovedEventHandlers = ArrayEx.Add(ContractRemovedEventHandlers, value);
            }
            remove
            {
                lock (LockObject)
                    ContractRemovedEventHandlers = ArrayEx.Remove(ContractRemovedEventHandlers, value);
            }
        }

        public event PluginEventHandler PluginRemoved
        {
            add
            {
                lock (LockObject)
                    PluginRemovedEventHandlers = ArrayEx.Add(PluginRemovedEventHandlers, value);
            }
            remove
            {
                lock (LockObject)
                    PluginRemovedEventHandlers = ArrayEx.Remove(PluginRemovedEventHandlers, value);
            }
        }

        protected internal TypeStore()
        {
            _contracts = new MetaElementCollection<Contract>();
            _plugins = new MetaElementCollection<Plugin>();
            contractDictionary = new Dictionary<string, MetaElementCollection<Contract>>();
            slotDefinitionDictionary = new Dictionary<string, MetaElementCollection<SlotDefinition>>();
            pluginDictionary = new Dictionary<string, MetaElementCollection<Plugin>>();
            extensionTypeDictionary = new Dictionary<string, MetaElementCollection<ExtensionType>>();
            plugTypeDictionary = new Dictionary<string, MetaElementCollection<PlugType>>();
            slotTypeDictionary = new Dictionary<string, MetaElementCollection<SlotType>>();
            Modifier = new TypeStoreModifier(this);
        }

        public IEnumerable<Contract> GetContracts(string name)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            lock (LockObject)
                return InternalGetContracts(name);
        }

        public IEnumerable<Plugin> GetPlugins(string name)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            lock (LockObject)
                return InternalGetPlugins(name);
        }

        public IEnumerable<SlotDefinition> GetSlotDefinitions(string name)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            lock (LockObject)
                return InternalGetSlotDefinitions(name);
        }

        public IEnumerable<ExtensionType> GetExtensionTypes(string name)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            lock (LockObject)
                return InternalGetExtensionTypes(name);
        }

        public IEnumerable<PlugType> GetPlugTypes(string name)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            lock (LockObject)
                return InternalGetPlugTypes(name);
        }

        public IEnumerable<SlotType> GetSlotTypes(string name)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            lock (LockObject)
                return InternalGetSlotTypes(name);
        }

        internal MetaElementCollection<Contract> InternalGetContracts(string name)
        {
            return contractDictionary.ContainsKey(name)
                ? contractDictionary[name]
                : new MetaElementCollection<Contract>();
        }

        internal MetaElementCollection<Plugin> InternalGetPlugins(string name)
        {
            return pluginDictionary.ContainsKey(name)
                ? pluginDictionary[name]
                : new MetaElementCollection<Plugin>();
        }

        internal MetaElementCollection<SlotDefinition> InternalGetSlotDefinitions(string name)
        {
            return slotDefinitionDictionary.ContainsKey(name)
                ? slotDefinitionDictionary[name]
                : new MetaElementCollection<SlotDefinition>();
        }

        internal MetaElementCollection<ExtensionType> InternalGetExtensionTypes(string name)
        {
            return extensionTypeDictionary.ContainsKey(name)
                ? extensionTypeDictionary[name]
                : new MetaElementCollection<ExtensionType>();
        }

        internal MetaElementCollection<PlugType> InternalGetPlugTypes(string name)
        {
            return plugTypeDictionary.ContainsKey(name)
                ? plugTypeDictionary[name]
                : new MetaElementCollection<PlugType>();
        }

        internal MetaElementCollection<SlotType> InternalGetSlotTypes(string name)
        {
            return slotTypeDictionary.ContainsKey(name)
                ? slotTypeDictionary[name]
                : new MetaElementCollection<SlotType>();
        }

        internal void OnContractAdded(Contract contract)
        {
            Logger.OnLog(
                this,
                Messages.TypeStore_AssemblyAdded,
                (object) "Contract",
                (object) contract.Id,
                (object) contract.Name,
                (object) contract.Uri);
            
            Notifier.OnContractAdded(new ContractEventArgs(contract));
        }

        internal void OnPluginAdded(Plugin plugin)
        {
            Logger.OnLog(
                this,
                Messages.TypeStore_AssemblyAdded,
                (object) "Plugin",
                (object) plugin.Id,
                (object) plugin.Name,
                (object) plugin.Uri);
            
            Notifier.OnPluginAdded(new PluginEventArgs(plugin));
            Composer.OnPluginAdded(plugin);
        }

        internal void OnContractRemoved(Contract contract)
        {
            Logger.OnLog(
                this,
                Messages.TypeStore_AssemblyRemoved,
                (object) "Contract",
                (object) contract.Id,
                (object) contract.Name,
                (object) contract.Uri);
            
            Notifier.OnContractRemoved(new ContractEventArgs(contract));
        }

        internal void OnPluginRemoved(Plugin plugin)
        {
            Logger.OnLog(
                this,
                Messages.TypeStore_AssemblyRemoved,
                (object) "Plugin",
                (object) plugin.Id,
                (object) plugin.Name,
                (object) plugin.Uri);
            
            Notifier.OnPluginRemoved(new PluginEventArgs(plugin));
            Composer.OnPluginRemoved(plugin);
        }

        public class TypeStoreModifier : ITypeStoreModifier
        {
            protected readonly TypeStore store;
            internal readonly Queue<TypeStoreNotification> NotificationQueue;

            internal TypeStoreModifier(TypeStore store)
            {
                this.store = store;
                NotificationQueue = new Queue<TypeStoreNotification>();
            }

            public void Add(IEnumerable<Contract> contracts, IEnumerable<Plugin> plugins)
            {
                lock (store.LockObject)
                    InternalAdd(contracts, plugins);
            }

            public void Remove(IEnumerable<Contract> contracts, IEnumerable<Plugin> plugins)
            {
                lock (store.LockObject)
                    InternalRemove(contracts, plugins);
            }

            internal void InternalAdd(IEnumerable<Contract> contracts, IEnumerable<Plugin> plugins)
            {
                var q = NotificationQueue.Count == 0;

                if (contracts != null)
                    foreach (var contract in contracts)
                        AddContract(contract);

                if (plugins != null)
                    foreach (var plugin in plugins)
                        AddPlugin(plugin);

                if (!q)
                    return;

                while (NotificationQueue.Count > 0)
                    NotificationQueue.Dequeue().Notify();
            }

            internal void InternalRemove(IEnumerable<Contract> contracts, IEnumerable<Plugin> plugins)
            {
                var q = NotificationQueue.Count == 0;

                if (contracts != null)
                    foreach (var contract in contracts)
                        RemoveContract(contract);

                if (plugins != null)
                    foreach (var plugin in plugins)
                        RemovePlugin(plugin);

                if (!q)
                    return;

                while (NotificationQueue.Count > 0)
                    NotificationQueue.Dequeue().Notify();
            }

            private bool SetRuntime(AssemblyElement element)
            {
                if (element.runtime != null && element.runtime != store.Runtime)
                    return false;

                element.runtime = store.Runtime;
                
                return true;
            }

            private void AddContract(Contract contract)
            {
                if (!SetRuntime(contract))
                    return;

                MetaElementCollection<Contract> elementCollection;

                if (store.contractDictionary.ContainsKey(contract.Name))
                {
                    elementCollection = store.contractDictionary[contract.Name];
                    using (var cEnumerator = elementCollection.
                        Where(existingContract => contract.Uri.Equals(existingContract.Uri)).GetEnumerator())
                    {
                        if (cEnumerator.MoveNext())
                        {
                            var current = cEnumerator.Current;

                            if (contract.LastModified.Equals(current.LastModified))
                                return;

                            RemoveContract(current);
                        }
                    }
                }
                else
                    elementCollection = store.contractDictionary[contract.Name] =
                        new MetaElementCollection<Contract>();

                store._contracts.Add(contract);
                elementCollection.Add(contract);

                foreach (var slotDef in contract.SlotDefinitions)
                    AddSlotDefinition(slotDef);
                
                NotificationQueue.Enqueue(new ContractNotification(store.OnContractAdded, contract));
            }

            private void AddSlotDefinition(SlotDefinition slotDefinition)
            {
                (!store.slotDefinitionDictionary.ContainsKey(slotDefinition.Name)
                    ? (store.slotDefinitionDictionary[slotDefinition.Name] =
                        new MetaElementCollection<SlotDefinition>())
                    : store.slotDefinitionDictionary[slotDefinition.Name]).Add(slotDefinition);
            }

            private void AddPlugin(Plugin plugin)
            {
                if (!SetRuntime(plugin))
                    return;

                MetaElementCollection<Plugin> elementCollection;

                if (store.pluginDictionary.ContainsKey(plugin.Name))
                {
                    elementCollection = store.pluginDictionary[plugin.Name];

                    using (var pEnumerator = elementCollection.
                        Where(existingPlugin => plugin.Uri.Equals(existingPlugin.Uri)).GetEnumerator())
                    {
                        if (pEnumerator.MoveNext())
                        {
                            var current = pEnumerator.Current;

                            if (plugin.LastModified.Equals(current.LastModified))
                                return;
                            
                            RemovePlugin(current);
                        }
                    }
                }
                else
                    elementCollection = store.pluginDictionary[plugin.Name] =
                        new MetaElementCollection<Plugin>();

                store._plugins.Add(plugin);
                elementCollection.Add(plugin);
                
                foreach (var extType in plugin.ExtensionTypes)
                    AddExtensionType(extType);
                
                NotificationQueue.Enqueue(new PluginNotification(store.OnPluginAdded, plugin));
            }

            private void AddExtensionType(ExtensionType extensionType)
            {
                (!store.extensionTypeDictionary.ContainsKey(extensionType.Name)
                    ? (store.extensionTypeDictionary[extensionType.Name] =
                        new MetaElementCollection<ExtensionType>())
                    : store.extensionTypeDictionary[extensionType.Name]).Add(extensionType);
                
                foreach (var plugType in extensionType.PlugTypes)
                    AddPlugType(plugType);
                
                foreach (var slotType in extensionType.SlotTypes)
                    AddSlotType(slotType);
            }

            private void AddPlugType(PlugType plugType)
            {
                (!store.plugTypeDictionary.ContainsKey(plugType.Name)
                    ? (store.plugTypeDictionary[plugType.Name] = new MetaElementCollection<PlugType>())
                    : store.plugTypeDictionary[plugType.Name]).Add(plugType);
            }

            private void AddSlotType(SlotType slotType)
            {
                (!store.slotTypeDictionary.ContainsKey(slotType.Name)
                    ? (store.slotTypeDictionary[slotType.Name] = new MetaElementCollection<SlotType>())
                    : store.slotTypeDictionary[slotType.Name]).Add(slotType);
            }

            private void RemoveContract(Contract contract)
            {
                store._contracts.Remove(contract);
                store.contractDictionary.Remove(contract.Name);
                
                foreach (var slotDef in contract.SlotDefinitions)
                    RemoveSlotDefinition(slotDef);
                
                NotificationQueue.Enqueue(new ContractNotification(store.OnContractRemoved, contract));
            }

            private void RemoveSlotDefinition(SlotDefinition slotDefinition)
            {
                store.slotDefinitionDictionary[slotDefinition.Name].Remove(slotDefinition);
            }

            private void RemovePlugin(Plugin plugin)
            {
                store._plugins.Remove(plugin);
                store.pluginDictionary.Remove(plugin.Name);
                
                foreach (var extType in plugin.ExtensionTypes)
                    RemoveExtensionType(extType);
                
                NotificationQueue.Enqueue(new PluginNotification(store.OnPluginRemoved, plugin));
            }

            private void RemoveExtensionType(ExtensionType extensionType)
            {
                store.extensionTypeDictionary[extensionType.Name].Remove(extensionType);
                
                foreach (var plugType in extensionType.PlugTypes)
                    RemovePlugType(plugType);
                
                foreach (var slotType in extensionType.SlotTypes)
                    RemoveSlotType(slotType);
            }

            private void RemovePlugType(PlugType plugType)
            {
                store.plugTypeDictionary[plugType.Name].Remove(plugType);
            }

            private void RemoveSlotType(SlotType slotType)
            {
                store.slotTypeDictionary[slotType.Name].Remove(slotType);
            }
        }
    }
}

